perf: hot-path store reads and translation processing - #18
Draft
jarda-svoboda wants to merge 6 commits into
Draft
Conversation
jarda-svoboda
marked this pull request as draft
July 10, 2026 01:00
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
2 times, most recently
from
July 11, 2026 14:58
3aede8f to
e146dac
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
2 times, most recently
from
July 25, 2026 12:37
8a1417e to
fa628ba
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 25, 2026 15:04
ad5f220 to
a024e37
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 25, 2026 15:06
fa628ba to
55c3afb
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 25, 2026 15:53
a024e37 to
1844911
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 25, 2026 15:54
55c3afb to
f3b40e4
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 25, 2026 23:47
1844911 to
693df58
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 26, 2026 00:01
f3b40e4 to
9b574a0
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 26, 2026 11:40
693df58 to
7935d68
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 26, 2026 11:53
9b574a0 to
50866c1
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 26, 2026 16:11
7935d68 to
68d31ba
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 26, 2026 16:11
50866c1 to
bfbf97c
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 26, 2026 17:28
68d31ba to
b47f600
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 26, 2026 17:29
bfbf97c to
6e7186d
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 26, 2026 17:32
b47f600 to
9c7bd64
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
2 times, most recently
from
July 26, 2026 17:41
ea55550 to
31f88f0
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 26, 2026 22:30
680c19a to
83b4d74
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 26, 2026 22:32
31f88f0 to
75045f2
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 26, 2026 23:38
83b4d74 to
41e2fbf
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 26, 2026 23:40
75045f2 to
47c7cd6
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 27, 2026 00:14
41e2fbf to
f236673
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 27, 2026 00:15
47c7cd6 to
21ac7fa
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 27, 2026 00:24
21ac7fa to
7bb9de6
Compare
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 27, 2026 00:48
f236673 to
0ee6479
Compare
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 27, 2026 00:48
7bb9de6 to
b01860e
Compare
The computed-key spread per key copied the accumulator on every step, which is quadratic in the number of keys and the dominant cost of every translation load. The accumulator is mutated instead — function-local, null-prototype so a literal '__proto__' key stays an own property rather than reaching the prototype setter, and finished with a single spread that restores a normal object with DefineProperty semantics.
`Intl.Collator.supportedLocalesOf` is expensive and the same locales repeat on every store recompute. Successful lookups are cached LRU (bounded, reinserted on hit, so a flood of visitor-supplied locales evicts itself rather than the app's own). Only string inputs are cacheable — for any other input the string form is not a faithful key — and failures are never cached, so a locale Intl cannot resolve yet can recover and its warning stays tied to the call.
sanitizeLocales ran inside the .find() callbacks — once per candidate locale per lookup. Hoist the requested locale's sanitization, keep the fallback's inside the miss branch so it stays short-circuited, and skip both when there are no locales to match against: sanitizing there only emitted a non-standard warning for a lookup that cannot succeed.
get(store) on an inactive svelte derived re-initializes and tears down the entire dependency chain on every call, so each t.get()/l.get() re-ran the translation store callback — scaling every read with the translation table size (5255 ms for 10k reads of a 5000-key table; ~20 ms with warm chains). Hold no-op subscriptions to t, l and initialized so the chains stay active. locales stays cold on purpose: its callback re-sanitizes every loader locale, so warming it would turn the non-standard-locale warning from once per read into once per store write. Also default the destructured config in loading.toPromise, which is now reachable before any config is set.
jarda-svoboda
force-pushed
the
fix/locale-key-safety
branch
from
July 27, 2026 01:22
0ee6479 to
de73253
Compare
The constructor holds subscriptions for the instance's whole life (the loader trigger, promise purging, the warm derived chains) with no way to detach them, and a load started before teardown still wrote its result to the stores afterwards. destroy() detaches those subscriptions, clears pending promises, resets the loading flag, and marks the instance inactive: every entry point that would load or mutate now bails through one shared gate that warns, and results arriving from an in-flight load are dropped. Reads keep working. Documented in docs/README.md, including that it is not needed for garbage collection — the subscription graph is instance-internal.
The rule mandated the computed-key-spread accumulation style unconditionally, which toDotNotation now deviates from for performance. State what the code actually does: immutable style for shared state, mutation only into function-local null-prototype accumulators finished with a single spread, never on store state.
jarda-svoboda
force-pushed
the
perf/hot-paths
branch
from
July 27, 2026 01:22
b01860e to
eb37e49
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Performance work on the read and load hot paths. 6 commits, each independently green.
> Stacked on #19 — this PR targets
fix/locale-key-safety, so its diff shows only the performance changes. Merge #19 first; GitHub will retarget this tomasterautomatically.Commits
perf(utils): make toDotNotation linear in key count__proto__key an own property.perf(utils): cache successful locale sanitizationsIntl.Collator.supportedLocalesOfis expensive and the same locales repeat on every store recompute. Bounded LRU (1000 entries, reinserted on hit, so a flood of visitor-supplied locales evicts itself rather than the app's own). Only string inputs are cached; failures are never cached, so a locale Intl cannot resolve yet can recover and its warning stays tied to the call rather than to whichever logger and level happened to be installed first.perf(index): sanitize once per getLocale lookup, fallback lazily.find()predicates. Hoisted, fallback kept short-circuited, and both skipped when there is nothing to match against — sanitizing there only emitted a warning for a lookup that cannot succeed.perf(index): keep read-path derived chains warmget()on an inactive svelte derived re-initializes and tears down the whole chain, so everyt.get()re-ran the translation callback and scaled with table size. Holds no-op subscriptions tot,l,initialized.localesis deliberately left cold — see below.feat: add destroy() to stop loading and fence in-flight resultsdestroy()detaches them, clears pending promises, resets the loading flag, and refuses every entry point that would load —setLocale,setRoute,locale.set/locale.update,loadTranslations,loadConfig— with one warning naming the call the consumer actually made. Results arriving from an in-flight load are dropped by the loader's own fence. Reads and consumer-drivenaddTranslations()keep working: a destroyed instance stops loading, not storing.docs: allow function-local mutable accumulators on measured hot pathsWhy
localesstays coldIt is the only read-path chain whose callback touches consumer-supplied config objects (
loaders.map(({ locale }) => locale)). Warming it moves that read into a store flush, and svelte's subscriber queue does not unwind on a throw — a single malformed loader would then wedge store propagation process-wide instead of failing the one lookup that read it. Verified: warming it turns one bad loader into five unrelated test failures. There is a regression test pinning it.Measured (Node 22, svelte 5.55, built bundle)
t.get()× 10k, 5000-key tableaddTranslations(8000 keys)sanitizeLocales× 10k (4 locales)Testing
npm test— 70 tests; every commit verified green independently (59 / 61 / 61 / 63 / 70 / 70)npm run test:dist— 3 tests against the built ESM and CJS bundlesnpm run build,npm run typecheckandnpm run lintclean__proto__keys, warm-chain reference equality,localesleft cold, in-flight load dropped afterdestroy()(fails when the loader'sdestroyedfence is removed),locale.setafterdestroy()reported,loadTranslationsafterdestroy()reported once,loadConfigafterdestroy(), locale-cache success/failure contractNotes
hasKeyshelper (benchmarked no faster than theObject.keys().lengthit replaced) and aserialize/loader-key micro-optimization (negligible at realistic sizes, and it duplicated the accumulator pattern). Their removal also retired thesafeTable/sealTablehelpers — the null-prototype pattern now has exactly one site.level: 'error'silenced the diagnostic for every later instance in the process. The warning is back to once per call.addTranslations' per-load merge (consumers rely on immutable table references).loadedKeys), so §10 and §11 no longer contradict each other.Known follow-ups (non-blocking)
get()results, which pins svelte's derived caching rather than the mechanism itself. Deterministic today (verified red→green) but would need a subscription-counting probe to be implementation-independent.extendedDerived()factory owning both would make that structural.isLoadingandpromisesare two representations of "a load is in flight". Deriving the flag from the in-flight set would collapse them and remove the purge subscriber entirely — the source of two bugs fixed in fix: prototype-safe locale reads and loader error handling #19. Deliberately not done here: it is a design change, and it belongs with the store-layer work planned for the next major.dist/index.cjscannot be loaded on a Node old enough to rejectrequire()of an ESM dependency, because svelte 5 ships its store as ESM only. Pre-existing packaging issue; the bundle spec skips that assertion where it applies rather than papering over it.